angular.module('ui.bootstrap.dateparser', [])

  .service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function ($log, $locale, dateFilter, orderByFilter, filterFilter) {
    // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
    var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;

    var localeId;
    var formatCodeToRegex;

    this.init = function () {
      localeId = $locale.id;
      this.parsers = {};
      this.formatters = {};

      formatCodeToRegex = [{
          key: 'yyyy',
          regex: '\\d{4}',
          apply: function (value) {
            this.year = +value;
          },
          formatter: function (date) {
            var _date = new Date();
            _date.setFullYear(Math.abs(date.getFullYear()));
            return dateFilter(_date, 'yyyy');
          }
        },
        {
          key: 'yy',
          regex: '\\d{2}',
          apply: function (value) {
            value = +value;
            this.year = value < 69 ? value + 2000 : value + 1900;
          },
          formatter: function (date) {
            var _date = new Date();
            _date.setFullYear(Math.abs(date.getFullYear()));
            return dateFilter(_date, 'yy');
          }
        },
        {
          key: 'y',
          regex: '\\d{1,4}',
          apply: function (value) {
            this.year = +value;
          },
          formatter: function (date) {
            var _date = new Date();
            _date.setFullYear(Math.abs(date.getFullYear()));
            return dateFilter(_date, 'y');
          }
        },
        {
          key: 'M!',
          regex: '0?[1-9]|1[0-2]',
          apply: function (value) {
            this.month = value - 1;
          },
          formatter: function (date) {
            var value = date.getMonth();
            if (/^[0-9]$/.test(value)) {
              return dateFilter(date, 'MM');
            }

            return dateFilter(date, 'M');
          }
        },
        {
          key: 'MMMM',
          regex: $locale.DATETIME_FORMATS_CN.MONTH.join('|'),
          apply: function (value) {
            this.month = $locale.DATETIME_FORMATS_CN.MONTH.indexOf(value);
          },
          formatter: function (date) {
            return dateFilter(date, 'MMMM');
          }
        },
        {
          key: 'MMM',
          regex: $locale.DATETIME_FORMATS_CN.SHORTMONTH.join('|'),
          apply: function (value) {
            this.month = $locale.DATETIME_FORMATS_CN.SHORTMONTH.indexOf(value);
          },
          formatter: function (date) {
            return dateFilter(date, 'MMM');
          }
        },
        {
          key: 'MM',
          regex: '0[1-9]|1[0-2]',
          apply: function (value) {
            this.month = value - 1;
          },
          formatter: function (date) {
            return dateFilter(date, 'MM');
          }
        },
        {
          key: 'M',
          regex: '[1-9]|1[0-2]',
          apply: function (value) {
            this.month = value - 1;
          },
          formatter: function (date) {
            return dateFilter(date, 'M');
          }
        },
        {
          key: 'd!',
          regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
          apply: function (value) {
            this.date = +value;
          },
          formatter: function (date) {
            var value = date.getDate();
            if (/^[1-9]$/.test(value)) {
              return dateFilter(date, 'dd');
            }

            return dateFilter(date, 'd');
          }
        },
        {
          key: 'dd',
          regex: '[0-2][0-9]{1}|3[0-1]{1}',
          apply: function (value) {
            this.date = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'dd');
          }
        },
        {
          key: 'd',
          regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
          apply: function (value) {
            this.date = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'd');
          }
        },
        {
          key: 'EEEE',
          regex: $locale.DATETIME_FORMATS_CN.DAY.join('|'),
          formatter: function (date) {
            return dateFilter(date, 'EEEE');
          }
        },
        {
          key: 'EEE',
          regex: $locale.DATETIME_FORMATS_CN.SHORTDAY.join('|'),
          formatter: function (date) {
            return dateFilter(date, 'EEE');
          }
        },
        {
          key: 'HH',
          regex: '(?:0|1)[0-9]|2[0-3]',
          apply: function (value) {
            this.hours = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'HH');
          }
        },
        {
          key: 'hh',
          regex: '0[0-9]|1[0-2]',
          apply: function (value) {
            this.hours = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'hh');
          }
        },
        {
          key: 'H',
          regex: '1?[0-9]|2[0-3]',
          apply: function (value) {
            this.hours = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'H');
          }
        },
        {
          key: 'h',
          regex: '[0-9]|1[0-2]',
          apply: function (value) {
            this.hours = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'h');
          }
        },
        {
          key: 'mm',
          regex: '[0-5][0-9]',
          apply: function (value) {
            this.minutes = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'mm');
          }
        },
        {
          key: 'm',
          regex: '[0-9]|[1-5][0-9]',
          apply: function (value) {
            this.minutes = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'm');
          }
        },
        {
          key: 'sss',
          regex: '[0-9][0-9][0-9]',
          apply: function (value) {
            this.milliseconds = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'sss');
          }
        },
        {
          key: 'ss',
          regex: '[0-5][0-9]',
          apply: function (value) {
            this.seconds = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 'ss');
          }
        },
        {
          key: 's',
          regex: '[0-9]|[1-5][0-9]',
          apply: function (value) {
            this.seconds = +value;
          },
          formatter: function (date) {
            return dateFilter(date, 's');
          }
        },
        {
          key: 'a',
          regex: $locale.DATETIME_FORMATS_CN.AMPMS.join('|'),
          apply: function (value) {
            if (this.hours === 12) {
              this.hours = 0;
            }

            if (value === 'PM') {
              this.hours += 12;
            }
          },
          formatter: function (date) {
            return dateFilter(date, 'a');
          }
        },
        {
          key: 'Z',
          regex: '[+-]\\d{4}',
          apply: function (value) {
            var matches = value.match(/([+-])(\d{2})(\d{2})/),
              sign = matches[1],
              hours = matches[2],
              minutes = matches[3];
            this.hours += toInt(sign + hours);
            this.minutes += toInt(sign + minutes);
          },
          formatter: function (date) {
            return dateFilter(date, 'Z');
          }
        },
        {
          key: 'ww',
          regex: '[0-4][0-9]|5[0-3]',
          formatter: function (date) {
            return dateFilter(date, 'ww');
          }
        },
        {
          key: 'w',
          regex: '[0-9]|[1-4][0-9]|5[0-3]',
          formatter: function (date) {
            return dateFilter(date, 'w');
          }
        },
        {
          key: 'GGGG',
          regex: $locale.DATETIME_FORMATS_CN.ERANAMES.join('|').replace(/\s/g, '\\s'),
          formatter: function (date) {
            return dateFilter(date, 'GGGG');
          }
        },
        {
          key: 'GGG',
          regex: $locale.DATETIME_FORMATS_CN.ERAS.join('|'),
          formatter: function (date) {
            return dateFilter(date, 'GGG');
          }
        },
        {
          key: 'GG',
          regex: $locale.DATETIME_FORMATS_CN.ERAS.join('|'),
          formatter: function (date) {
            return dateFilter(date, 'GG');
          }
        },
        {
          key: 'G',
          regex: $locale.DATETIME_FORMATS_CN.ERAS.join('|'),
          formatter: function (date) {
            return dateFilter(date, 'G');
          }
        }
      ];

      if (angular.version.major >= 1 && angular.version.minor > 4) {
        formatCodeToRegex.push({
          key: 'LLLL',
          regex: $locale.DATETIME_FORMATS_CN.STANDALONEMONTH.join('|'),
          apply: function (value) {
            this.month = $locale.DATETIME_FORMATS_CN.STANDALONEMONTH.indexOf(value);
          },
          formatter: function (date) {
            return dateFilter(date, 'LLLL');
          }
        });
      }
    };

    this.init();

    function getFormatCodeToRegex(key) {
      return filterFilter(formatCodeToRegex, {
        key: key
      }, true)[0];
    }

    this.getParser = function (key) {
      var f = getFormatCodeToRegex(key);
      return f && f.apply || null;
    };

    this.overrideParser = function (key, parser) {
      var f = getFormatCodeToRegex(key);
      if (f && angular.isFunction(parser)) {
        this.parsers = {};
        f.apply = parser;
      }
    }.bind(this);

    function createParser(format) {
      var map = [],
        regex = format.split('');

      // check for literal values
      var quoteIndex = format.indexOf('\'');
      if (quoteIndex > -1) {
        var inLiteral = false;
        format = format.split('');
        for (var i = quoteIndex; i < format.length; i++) {
          if (inLiteral) {
            if (format[i] === '\'') {
              if (i + 1 < format.length && format[i + 1] === '\'') { // escaped single quote
                format[i + 1] = '$';
                regex[i + 1] = '';
              } else { // end of literal
                regex[i] = '';
                inLiteral = false;
              }
            }
            format[i] = '$';
          } else {
            if (format[i] === '\'') { // start of literal
              format[i] = '$';
              regex[i] = '';
              inLiteral = true;
            }
          }
        }

        format = format.join('');
      }

      angular.forEach(formatCodeToRegex, function (data) {
        var index = format.indexOf(data.key);

        if (index > -1) {
          format = format.split('');

          regex[index] = '(' + data.regex + ')';
          format[index] = '$'; // Custom symbol to define consumed part of format
          for (var i = index + 1, n = index + data.key.length; i < n; i++) {
            regex[i] = '';
            format[i] = '$';
          }
          format = format.join('');

          map.push({
            index: index,
            key: data.key,
            apply: data.apply,
            matcher: data.regex
          });
        }
      });

      return {
        regex: new RegExp('^' + regex.join('') + '$'),
        map: orderByFilter(map, 'index')
      };
    }

    function createFormatter(format) {
      var formatters = [];
      var i = 0;
      var formatter, literalIdx;
      while (i < format.length) {
        if (angular.isNumber(literalIdx)) {
          if (format.charAt(i) === '\'') {
            if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') {
              formatters.push(constructLiteralFormatter(format, literalIdx, i));
              literalIdx = null;
            }
          } else if (i === format.length) {
            while (literalIdx < format.length) {
              formatter = constructFormatterFromIdx(format, literalIdx);
              formatters.push(formatter);
              literalIdx = formatter.endIdx;
            }
          }

          i++;
          continue;
        }

        if (format.charAt(i) === '\'') {
          literalIdx = i;
          i++;
          continue;
        }

        formatter = constructFormatterFromIdx(format, i);

        formatters.push(formatter.parser);
        i = formatter.endIdx;
      }

      return formatters;
    }

    function constructLiteralFormatter(format, literalIdx, endIdx) {
      return function () {
        return format.substr(literalIdx + 1, endIdx - literalIdx - 1);
      };
    }

    function constructFormatterFromIdx(format, i) {
      var currentPosStr = format.substr(i);
      for (var j = 0; j < formatCodeToRegex.length; j++) {
        if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) {
          var data = formatCodeToRegex[j];
          return {
            endIdx: i + data.key.length,
            parser: data.formatter
          };
        }
      }

      return {
        endIdx: i + 1,
        parser: function () {
          return currentPosStr.charAt(0);
        }
      };
    }

    this.filter = function (date, format) {
      if (!angular.isDate(date) || isNaN(date) || !format) {
        return date;
      }
      format = $locale.DATETIME_FORMATS_CN[format] || format;
      if ($locale.id !== localeId) {
        this.init();
      }

      if (!this.formatters[format]) {
        this.formatters[format] = createFormatter(format);
      }

      var formatters = this.formatters[format];
      return formatters.reduce(function (str, formatter) {
        return str + formatter(date);
      }, '');
    };

    this.parse = function (input, format, baseDate) {
      if (!angular.isString(input) || !format) {
        return input;
      }

      format = $locale.DATETIME_FORMATS_CN[format] || format;
      format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');

      if ($locale.id !== localeId) {
        this.init();
      }

      if (!this.parsers[format]) {
        this.parsers[format] = createParser(format, 'apply');
      }

      var parser = this.parsers[format],
        regex = parser.regex,
        map = parser.map,
        results = input.match(regex),
        tzOffset = false;
      if (results && results.length) {
        var fields, dt;
        if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
          fields = {
            year: baseDate.getFullYear(),
            month: baseDate.getMonth(),
            date: baseDate.getDate(),
            hours: baseDate.getHours(),
            minutes: baseDate.getMinutes(),
            seconds: baseDate.getSeconds(),
            milliseconds: baseDate.getMilliseconds()
          };
        } else {
          if (baseDate) {
            $log.warn('dateparser:', 'baseDate is not a valid date');
          }
          fields = {
            year: 1900,
            month: 0,
            date: 1,
            hours: 0,
            minutes: 0,
            seconds: 0,
            milliseconds: 0
          };
        }

        for (var i = 1, n = results.length; i < n; i++) {
          var mapper = map[i - 1];
          if (mapper.matcher === 'Z') {
            tzOffset = true;
          }

          if (mapper.apply) {
            mapper.apply.call(fields, results[i]);
          }
        }

        var datesetter = tzOffset ? Date.prototype.setUTCFullYear :
          Date.prototype.setFullYear;
        var timesetter = tzOffset ? Date.prototype.setUTCHours :
          Date.prototype.setHours;

        if (isValid(fields.year, fields.month, fields.date)) {
          if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) {
            dt = new Date(baseDate);
            datesetter.call(dt, fields.year, fields.month, fields.date);
            timesetter.call(dt, fields.hours, fields.minutes,
              fields.seconds, fields.milliseconds);
          } else {
            dt = new Date(0);
            datesetter.call(dt, fields.year, fields.month, fields.date);
            timesetter.call(dt, fields.hours || 0, fields.minutes || 0,
              fields.seconds || 0, fields.milliseconds || 0);
          }
        }

        return dt;
      }
    };

    // Check if date is valid for specific month (and year for February).
    // Month: 0 = Jan, 1 = Feb, etc
    function isValid(year, month, date) {
      if (date < 1) {
        return false;
      }

      if (month === 1 && date > 28) {
        return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0);
      }

      if (month === 3 || month === 5 || month === 8 || month === 10) {
        return date < 31;
      }

      return true;
    }

    function toInt(str) {
      return parseInt(str, 10);
    }

    this.toTimezone = toTimezone;
    this.fromTimezone = fromTimezone;
    this.timezoneToOffset = timezoneToOffset;
    this.addDateMinutes = addDateMinutes;
    this.convertTimezoneToLocal = convertTimezoneToLocal;

    function toTimezone(date, timezone) {
      return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
    }

    function fromTimezone(date, timezone) {
      return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
    }

    //https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207
    function timezoneToOffset(timezone, fallback) {
      timezone = timezone.replace(/:/g, '');
      var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
      return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
    }

    function addDateMinutes(date, minutes) {
      date = new Date(date.getTime());
      date.setMinutes(date.getMinutes() + minutes);
      return date;
    }

    function convertTimezoneToLocal(date, timezone, reverse) {
      reverse = reverse ? -1 : 1;
      var dateTimezoneOffset = date.getTimezoneOffset();
      var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
      return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
    }
  }]);